﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Net;
using System.Reflection;
using System.Net.Sockets;
using System.IO;

namespace LibGreen.Utility
{
	/// <summary>
    /// Describes the types of network proxies that shaim plugins should support
    /// </summary>
    public enum ShaimProxyType
    {
        /// <summary>
        /// Direct connection to the network
        /// </summary>
        None,
        /// <summary>
        /// Route socket connections through a SOCKS5 proxy
        /// </summary>
        Socks5
    }

    /// <summary>
    /// Describes the valid SOCKS5 authentication methods
    /// </summary>
    enum Socks5AuthType : byte
    {
        None = 0x00,
        GssApi = 0x01,
        UsernamePassword = 0x02,
        AuthMethodRejected = 0xFF
    }
	/// <summary>
	/// The call back function for the created socket
	/// </summary>
	/// <param name="soc">the Socket created</param>
	/// <param name="error">error message(blank if no error)</param>
	public delegate void ConnectionCreatedHandler (Socket soc, string error);

    public static class SocketFactory
    {
		private static ProxyInfo _proxyinfo;
        /// <summary>
        /// Gets the global proxy through which protocol plugins connect to the Internet
        /// </summary>
		//public static ProxyInfo GlobalProxySetting
		//{
		//    get { return _proxyinfo; }
		//    set { _proxyinfo = value; }
		//}


        /// <summary>
        /// Creates a new socket that is connected through the proxy defined in the global settings
        /// </summary>
        /// <param name="host">The host to which to connect through the proxy</param>
        /// <param name="port">The port to which to connect through the proxy</param>
        /// <param name="callback">A callback through which to receive information about the connected socket</param>
        /// <remarks>
        /// The <paramref name="callback"/> parameter must be a delegate that accepts a Socket and a string as its parameters.
        /// If the string parameter is a blank string, the connection operation succeeded and the socket parameter is usable.
        /// Otherwise, the string parameter contains the specific error message that occured during connection, and the
        /// socket parameter is an invalid connection.
        /// </remarks>
		public static void Connect (string host, int port, ConnectionCreatedHandler callback)
		{
		    if (callback == null)
		    {
		        throw new ArgumentNullException("argumentNullException");
		    }

		    // Make sure the callback is the right type
		    ParameterInfo[] param = callback.Method.GetParameters();
		    if (param.Length != 2 || param[0].ParameterType != typeof(Socket) || param[1].ParameterType != typeof(string))
		    {
		        throw new ArgumentException("proxyBadDelegate");
		    }

		    SocketConnectionData scd = new SocketConnectionData(host, port, callback);
		    scd.Socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);

		    string proxiedhost;
		    //if (GlobalProxySetting.ProxyType == ShaimProxyType.None)
		    //{
		        proxiedhost = scd.Server;
		   // }
		  //  else
		  //  {
		   //     proxiedhost = GlobalProxySetting.Server;
		  //  }

		    // Check to see if it's an IP address; if it is, don't bother with the DNS lookup
		    IPAddress ipAddress = null;
		    if (IPAddress.TryParse(proxiedhost, out ipAddress))
		    {
		        ConnectToIPAddress(scd, ipAddress);
		    }
		    else
		    {
		        try
		        {
		            Dns.BeginGetHostEntry(proxiedhost, CreateGPSEndDnsLookup, scd);
		        }
		        catch (SocketException sockex)
		        {
		            string message = "Cannot connect to DNS service:" + Environment.NewLine +
		              sockex.Message;
		            callback.DynamicInvoke(scd.Socket, message);
		        }
		    }
		}

        /// <summary>
        /// Ends the DNS lookup phase of connection and begins connecting the socket to the proxy host
        /// </summary>
        private static void CreateGPSEndDnsLookup(IAsyncResult res)
        {
            SocketConnectionData scd = res.AsyncState as SocketConnectionData;
            IPHostEntry hosts = null;
            String errorMessage = String.Empty;
            try
            {
                hosts = Dns.EndGetHostEntry(res);
            }
            catch (SocketException se)
            {
                // This has an odd habit of occuring for me when I use the MSN plugin
                // It's handled by the fallback DNS lookup below
                errorMessage = se.Message;
            }

            if (hosts == null || hosts.AddressList == null || hosts.AddressList.Length == 0)
            {
                // Fallback to a different synchronous DNS lookup method
                errorMessage = String.Empty;
                try
                {
                    hosts = new IPHostEntry();
                    if (scd != null) hosts.AddressList = Dns.GetHostAddresses(scd.Server);
                    if (hosts.AddressList == null || hosts.AddressList.Length == 0)
                    {
                        throw new InvalidDataException();
                    }
                }
                catch (SocketException sex)
                {
                    // Well shit
                    errorMessage = "Cannot resolve server:" + Environment.NewLine + sex.Message;
                }
                catch (InvalidDataException)
                {
                    errorMessage = "Cannot resolve server:" + Environment.NewLine + "Dns.GetHostAddresses returned no entries";
                }

                if (!String.IsNullOrEmpty(errorMessage) && scd != null)
                {
                    scd.Callback.DynamicInvoke(scd.Socket, errorMessage);
                    return;
                }
            }

            ConnectToIPAddress(scd, hosts.AddressList[0]);
        }

        /// <summary>
        /// Connects directly to an IP address
        /// </summary>
        /// <param name="scd">The SocketConnectionData carrying hostname information</param>
        /// <param name="address">The IP address to which to connect</param>
        private static void ConnectToIPAddress(SocketConnectionData scd, IPAddress address)
        {

            int port = 0;
			//if (GlobalProxySetting.ProxyType != ShaimProxyType.None)
			//{
			//    port = GlobalProxySetting.Port;
			//}
           // else
            //{
                if (scd != null) port = scd.Port;
           // }

            IPEndPoint ipep = new IPEndPoint(address, port);

            try
            {
                if (scd != null) scd.Socket.BeginConnect(ipep, CreateGPSEndInitialConnection, scd);
            }
            catch (SocketException sockex)
            {
                string message = "Cannot connect to server:" + Environment.NewLine +
                  sockex.Message;
                if (scd != null) scd.Callback.DynamicInvoke(scd.Socket, message);
            }
        }

        /// <summary>
        /// Ends the host connection phase and negotiates the proxied connection
        /// </summary>
        private static void CreateGPSEndInitialConnection(IAsyncResult res)
        {
            SocketConnectionData scd = null;
            String errorMessage = null;
            try
            {
                scd = res.AsyncState as SocketConnectionData;
                if (scd != null) scd.Socket.EndConnect(res);
            }
            catch (SocketException sockex)
            {
                errorMessage = "Can't connect to server: " + Environment.NewLine + sockex.Message;
            }

            if (!String.IsNullOrEmpty(errorMessage))
            {
                if (scd != null)
                    scd.Callback.DynamicInvoke(scd.Socket, errorMessage);
                return;
            }

			//if (GlobalProxySetting.ProxyType == ShaimProxyType.Socks5)
			//{
			//    Socks5AuthType auth = GetSocks5AuthMethod(scd);
			//    if (auth == Socks5AuthType.AuthMethodRejected)
			//    {
			//        if (scd != null) scd.Socket.Close();
			//        if (scd != null) scd.Callback.DynamicInvoke(scd.Socket, "SOCKS5 authentication mechanism unsupported");
			//        return;
			//    }
			//    else if (auth != Socks5AuthType.None)
			//    {
			//        if (SendSocks5Authentication(scd) == false)
			//        {
			//            if (scd != null) scd.Socket.Close();
			//            if (scd != null)
			//                scd.Callback.DynamicInvoke(scd.Socket, "SOCK5 authentication failed, bad username/password");
			//            return;
			//        }
			//    }

			//    if (SendSocks5ConnectRequest(scd) == false)
			//    {
			//        if (scd != null) scd.Socket.Close();
			//        if (scd != null) scd.Callback.DynamicInvoke(scd.Socket, "SOCKS5 connect failed, proxy server failure");
			//        return;
			//    }
			//}

            // Done!
            if (scd != null) scd.Callback.DynamicInvoke(scd.Socket, "");
        }

        /// <summary>
        /// Queries the SOCKS5 proxy for its desired authentication mechanism
        /// </summary>
        private static Socks5AuthType GetSocks5AuthMethod(SocketConnectionData scd)
        {
            int reqlength = 4, sent = 0;
            byte[] request = new byte[reqlength];
            request[0] = 0x05;
            request[1] = 0x02;
            request[2] = 0x00;
            request[3] = 0x02;
            while (sent < reqlength)
            {
                sent += scd.Socket.Send(request, sent, reqlength - sent, SocketFlags.None);
            }

            int replength = 2, received = 0;
            byte[] reply = new byte[replength];
            while (received < replength)
            {
                received += scd.Socket.Receive(reply, received, replength - received, SocketFlags.None);
            }

            return (Socks5AuthType)reply[1];
        }

        /// <summary>
        /// Sends the authentication sequence to the proxy server
        /// </summary>
		//private static bool SendSocks5Authentication(SocketConnectionData scd)
		//{
		//    int usernamelen = Encoding.ASCII.GetByteCount(GlobalProxySetting.Username);
		//    int passwordlen = Encoding.ASCII.GetByteCount(GlobalProxySetting.Password);
		//    int reqlength = 3 + usernamelen + passwordlen, sent = 0;
		//    byte[] request = new byte[reqlength];
		//    request[0] = 0x05;
		//    request[1] = (byte)usernamelen;
		//    Array.ConstrainedCopy(Encoding.ASCII.GetBytes(GlobalProxySetting.Username), 0, request, 2, usernamelen);
		//    request[2 + usernamelen] = (byte)passwordlen;
		//    Array.ConstrainedCopy(Encoding.ASCII.GetBytes(GlobalProxySetting.Password), 0, request, 3 + usernamelen, passwordlen);

		//    while (sent < reqlength)
		//    {
		//        sent += scd.Socket.Send(request, sent, reqlength - sent, SocketFlags.None);
		//    }

		//    int replength = 2, received = 0;
		//    byte[] reply = new byte[replength];
		//    while (received < replength)
		//    {
		//        received += scd.Socket.Receive(reply, received, replength - received, SocketFlags.None);
		//    }

		//    return (reply[1] == 0x00);
		//}

        /// <summary>
        /// Send a CONNECT request to the proxy server
        /// </summary>
        private static bool SendSocks5ConnectRequest(SocketConnectionData scd)
        {
            IPAddress ipaddr = null;
            try
            {
                ipaddr = Dns.GetHostEntry(scd.Server).AddressList[0];
            }
            catch (FormatException)
            {
                // We'll just use the domain name method then
            }

            int sent = 0;
            int iplength = (ipaddr == null) ? Encoding.ASCII.GetByteCount(scd.Server) :
              (ipaddr.AddressFamily == AddressFamily.InterNetwork) ? 4 : 6;

            int reqlength = 6 + iplength;
            byte[] request = new byte[reqlength];
            request[0] = 0x05;
            request[1] = 0x01;
            request[2] = 0x00;
            if (ipaddr == null)
            {
                request[3] = 0x03;
                Array.ConstrainedCopy(Encoding.ASCII.GetBytes(scd.Server), 0, request, 4, reqlength - 6);

            }
            else if (ipaddr.AddressFamily == AddressFamily.InterNetwork)
            {
                request[3] = 0x01;
                Array.ConstrainedCopy(ipaddr.GetAddressBytes(), 0, request, 4, reqlength - 6);
            }
            else
            {
                request[3] = 0x04;
                Array.ConstrainedCopy(ipaddr.GetAddressBytes(), 0, request, 4, reqlength - 6);
            }

            request[reqlength - 2] = (byte)((scd.Port & 0xFF00) >> 8);
            request[reqlength - 1] = (byte)(scd.Port & 0xFF);
            while (sent < reqlength)
            {
                sent += scd.Socket.Send(request, sent, reqlength - sent, SocketFlags.None);
            }

            byte[] reply = new byte[256]; // Variable length reply here
            scd.Socket.Receive(reply);
            return (reply[1] == 0x00);
        }
    }
}
